// Created by Roman Novitsky on 27.05.2009


#region Imported Namespaces


using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Xml;

using WtfManager.Framework;


#endregion


namespace WtfManager.UIManager
{
  public class MenuManager: ExtensibleService
  {
    #region Fields


    private static readonly List<MenuBase> _pendingMenus = new List<MenuBase>();

    public static readonly DependencyProperty IsDefaultProperty = DependencyProperty.RegisterAttached("IsDefault",
                                                                                                      typeof(bool),
                                                                                                      typeof(MenuManager
                                                                                                        ));

    public static readonly DependencyProperty IsHiddenProperty = DependencyProperty.RegisterAttached("IsHidden",
                                                                                                     typeof(bool),
                                                                                                     typeof(MenuManager));

    public static readonly DependencyProperty IsPinnedProperty = DependencyProperty.RegisterAttached("IsPinned",
                                                                                                     typeof(bool),
                                                                                                     typeof(MenuManager));


    public static readonly DependencyProperty ProvideMenuItemsProperty =
      DependencyProperty.RegisterAttached("ProvideMenuItems", typeof(bool), typeof(MenuManager),
                                          new PropertyMetadata(ProvideMenuItemsPropertyChanged));

    public static readonly DependencyProperty SettingsFileNameProperty =
      DependencyProperty.RegisterAttached("SettingsFileName", typeof(string), typeof(MenuManager),
                                          new PropertyMetadata(SettingsFileNamePropertyChanged));


    public static readonly DependencyProperty TargetCommandNameProperty =
      DependencyProperty.RegisterAttached("TargetCommandName", typeof(string), typeof(MenuManager));

    private readonly Dictionary<ICommand, IMenuManagerExtension> _extensions =
      new Dictionary<ICommand, IMenuManagerExtension>();

    private readonly Dictionary<MenuBase, MenuState> _menus = new Dictionary<MenuBase, MenuState>();
    private readonly Dictionary<string, List<MenuState>> _menusByFileName = new Dictionary<string, List<MenuState>>();
    private readonly Dictionary<IInputElement, MenuState> _menuTargets = new Dictionary<IInputElement, MenuState>();


    #endregion


    #region Constructors/Finalizers


    internal MenuManager()
    {
    }


    #endregion


    #region Nested type: MenuState


    private class MenuState
    {
      #region Fields


      private string _fileName;


      #endregion


      #region Constructors/Finalizers


      public MenuState(MenuBase menu, IInputElement target)
      {
        Menu = menu;
        Target = target;
      }


      #endregion


      #region Properties/Indexers


      public string FileName
      {
        get
        {
          return _fileName;
        }
        set
        {
          _fileName = value;
        }
      }


      public IInputElement LastFocusedElement
      {
        get;
        set;
      }

      public MenuBase Menu
      {
        get;
        private set;
      }


      public IInputElement Target
      {
        get;
        private set;
      }


      #endregion


      #region Methods


      public void Load()
      {
        if (string.IsNullOrEmpty(_fileName))
        {
          return;
        }

        FileStream stream = null;

        if (!File.Exists(_fileName))
        {
          return;
        }

        try
        {
          stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read);
          var state = (MenuBase)XamlReader.Load(stream);

          Menu.Items.Clear();

          state.MoveItems(Menu);
        }
        catch
        {
          // TODO <RNovitsky, 03.06.2009>: Log this
          return;
        }
        finally
        {
          if (stream != null)
          {
            stream.Close();
          }
        }
      }


      public void Save()
      {
        if (string.IsNullOrEmpty(_fileName))
        {
          return;
        }

        FileStream stream = null;
        XmlTextWriter xmlTextWriter = null;

        try
        {
          stream = new FileStream(_fileName, FileMode.Create, FileAccess.ReadWrite);

          xmlTextWriter = new XmlTextWriter(stream, Encoding.Default);
          xmlTextWriter.Formatting = Formatting.Indented;
          xmlTextWriter.Indentation = 2;

          Menu.PrepareToSaveState();

          var state = new Menu();
          Menu.MoveItems(state);

          XamlWriter.Save(state, xmlTextWriter);

          state.MoveItems(Menu);
        }
        catch
        {
          // TODO <RNovitsky, 29.05.2009>: Log this.
          return;
        }
        finally
        {
          if (xmlTextWriter != null)
          {
            xmlTextWriter.Close();
          }

          if (stream != null)
          {
            stream.Close();
          }
        }
      }


      #endregion
    }


    #endregion


    #region Properties/Indexers


    public override string Description
    {
      get
      {
        return "Generates menu structure automatically and provides menu items customization capabilities.";
      }
    }


    public override string Name
    {
      get
      {
        return "Menu Manager";
      }
    }


    #endregion


    #region Methods


    public static void SetSettingsFileName(DependencyObject element, string value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(SettingsFileNameProperty, value);
    }


    public static string GetSettingsFileName(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (string)element.GetValue(SettingsFileNameProperty);
    }


    private void ProcessMenu(MenuBase menu)
    {
      bool provideMenuItems = GetProvideMenuItems(menu);

      if (provideMenuItems)
      {
        AddMenu(menu);
        SetState(menu, GetSettingsFileName(menu));
      }
      else
      {
        RemoveMenu(menu);
      }
    }


    private void SetState(MenuBase menuBase, string settingsFileName)
    {
      MenuState menuState = _menus[menuBase];

      string fileName = menuState.FileName;

      if (!string.IsNullOrEmpty(fileName))
      {
        _menusByFileName[fileName].Remove(menuState);
      }

      menuState.FileName = settingsFileName;

      if (string.IsNullOrEmpty(settingsFileName))
      {
        return;
      }

      if (!_menusByFileName.ContainsKey(settingsFileName))
      {
        _menusByFileName.Add(settingsFileName, new List<MenuState>());
      }

      _menusByFileName[settingsFileName].Add(menuState);

      menuState.Load();
    }


    private void RemoveMenu(MenuBase menuBase)
    {
      if (!_menus.ContainsKey(menuBase))
      {
        return;
      }

      if (menuBase is Menu)
      {
        var menu = (Menu)menuBase;

        IInputElement element = UnregisterMenu(menu);

        element.GotKeyboardFocus -= MenuTargetGotKeyboardFocus;
      }
      else if (menuBase is ContextMenu)
      {
        var contextMenu = (ContextMenu)menuBase;

        UnregisterMenu(contextMenu);

        contextMenu.Opened -= ContextMenuOpened;
      }
    }


    private void AddMenu(MenuBase menuBase)
    {
      if (_menus.ContainsKey(menuBase))
      {
        return;
      }

      if (menuBase is Menu)
      {
        var menu = (Menu)menuBase;

        if (menu.IsMainMenu)
        {
          DependencyObject mainMenuTarget = GetMainMenuTarget(menu);
          var menuTarget = mainMenuTarget as IInputElement;

          if (menuTarget == null)
          {
            throw new InvalidOperationException();
          }

          RegisterMenu(menu, menuTarget);

          menuTarget.GotKeyboardFocus += MenuTargetGotKeyboardFocus;

          if (menuTarget.IsKeyboardFocused || menuTarget.IsKeyboardFocusWithin)
          {
            GenerateMenuItems(menu, menuTarget);
          }
        }
        else
        {
          throw new NotImplementedException("Only main menus supported.");
        }
      }
      else if (menuBase is ContextMenu)
      {
        var contextMenu = (ContextMenu)menuBase;

        RegisterMenu(contextMenu, null);

        contextMenu.Opened += ContextMenuOpened;
      }
      else
      {
        throw new NotImplementedException("Only main menus or context menus supported.");
      }
    }


    private void RegisterMenu(MenuBase menu, IInputElement menuTarget)
    {
      var menuInfo = new MenuState(menu, menuTarget);

      if (menuTarget != null)
      {
        _menuTargets.Add(menuTarget, menuInfo);
      }

      string fileName = menuInfo.FileName;

      if (!string.IsNullOrEmpty(fileName))
      {
        if (!_menusByFileName.ContainsKey(fileName))
        {
          _menusByFileName.Add(fileName, new List<MenuState>());
        }

        _menusByFileName[fileName].Add(menuInfo);
      }

      _menus.Add(menu, menuInfo);
    }


    private IInputElement UnregisterMenu(MenuBase menu)
    {
      MenuState menuState = _menus[menu];

      if (menuState.Target != null)
      {
        _menuTargets.Remove(menuState.Target);
      }

      string fileName = menuState.FileName;

      if (!string.IsNullOrEmpty(fileName))
      {
        _menusByFileName[fileName].Remove(menuState);
      }

      _menus.Remove(menu);

      return menuState.Target;
    }


    private void ContextMenuOpened(object sender, RoutedEventArgs e)
    {
      var contextMenu = (ContextMenu)sender;

      GenerateContextMenuItems(contextMenu, contextMenu.PlacementTarget);
    }


    private static DependencyObject GetMainMenuTarget(DependencyObject menu)
    {
      DependencyObject target;
      DependencyObject parent = menu;

      do
      {
        target = parent;
        parent = VisualTreeHelper.GetParent(target);
      }
      while (parent != null);

      return target;
    }


    private void MenuTargetGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
      var inputElement = (IInputElement)sender;

      if (!inputElement.IsKeyboardFocused && !inputElement.IsKeyboardFocusWithin)
      {
        return;
      }

      MenuState menuState = _menuTargets[inputElement];
      var menu = (Menu)menuState.Menu;

      if (menu.IsKeyboardFocused || menu.IsKeyboardFocusWithin)
      {
        return;
      }

      GenerateMenuItems(menu, inputElement);
    }


    private void RefreshMenu(MenuBase menu)
    {
      MenuState menuState = _menus[menu];

      menuState.Load();

      if (menu is Menu)
      {
        GenerateMenuItems(menu, menuState.LastFocusedElement);
      }
    }


    private void GenerateMenuItems(MenuBase menu, IInputElement target)
    {
      MenuState menuState = _menus[menu];
      menuState.LastFocusedElement = target;

      var commands = new List<MenuItemCommand>();

      GatherCommandBindings(Keyboard.FocusedElement as DependencyObject, commands);
      GatherInputBindings(target as UIElement, commands);

      BuildMenu(menu, commands);
    }


    private void BuildMenu(MenuBase menu, List<MenuItemCommand> commands)
    {
      if (!menu.Build(commands))
      {
        return;
      }

      MenuState menuState = _menus[menu];
      menuState.Save();

      if (string.IsNullOrEmpty(menuState.FileName))
      {
        return;
      }

      foreach (MenuState state in _menusByFileName[menuState.FileName])
      {
        if (state == menuState)
        {
          continue;
        }

        RefreshMenu(menu);
      }
    }


    private void GenerateContextMenuItems(MenuBase menu, IInputElement target)
    {
      var commands = new List<MenuItemCommand>();

      GetCommandBindings(target as DependencyObject, commands);
      GetInputBindings(target as DependencyObject, commands);

      BuildMenu(menu, commands);
    }


    private void GatherCommandBindings(DependencyObject target, List<MenuItemCommand> commands)
    {
      if (target == null)
      {
        return;
      }

      do
      {
        if (target is FrameworkElement)
        {
          GetCommandBindings(target, commands);
        }

        target = VisualTreeHelper.GetParent(target);
      }
      while (target != null);
    }


    private void GatherInputBindings(UIElement target, List<MenuItemCommand> commands)
    {
      if (target == null)
      {
        return;
      }

      if (!target.IsKeyboardFocused && !target.IsKeyboardFocusWithin)
      {
        return;
      }

      int childrenCount = VisualTreeHelper.GetChildrenCount(target);

      for (int i = 0; i < childrenCount; i++)
      {
        DependencyObject child = VisualTreeHelper.GetChild(target, i);

        GatherInputBindings(child as UIElement, commands);
      }

      GetInputBindings(target, commands);
    }


    private void GetCommandBindings(DependencyObject currentItem, List<MenuItemCommand> commands)
    {
      var element = currentItem as UIElement;

      if (element == null)
      {
        return;
      }

      foreach (CommandBinding commandBinding in element.CommandBindings)
      {
        AddMenuItems(commands, element, commandBinding.Command, null, null);
      }
    }


    private void GetInputBindings(DependencyObject currentItem, List<MenuItemCommand> commands)
    {
      var element = currentItem as UIElement;

      if (element == null)
      {
        return;
      }

      foreach (InputBinding inputBinding in element.InputBindings)
      {
        AddMenuItems(commands, element, inputBinding.Command, inputBinding.CommandParameter, inputBinding.Gesture);
      }
    }


    private void AddMenuItems(List<MenuItemCommand> commands, IInputElement element, ICommand command, object commandParameter, InputGesture inputGesture)
    {
      AddMenuItem(commands, command, element, commandParameter, inputGesture);

      List<object> parameters = GetAllPossibleParameters(command, element);

      foreach (object parameter in parameters)
      {
        AddMenuItem(commands, command, element, parameter, null);
      }
    }


    private List<object> GetAllPossibleParameters(ICommand command, IInputElement element)
    {
      if(_extensions.ContainsKey(command))
      {
        IMenuManagerExtension extension = _extensions[command];

        return extension.GetAllPossibleParameters(element);
      }

      return new List<object>();
    }


    private void AddMenuItem(List<MenuItemCommand> commands, ICommand command, IInputElement commandTarget,
                             object parameter, InputGesture gesture)
    {
      if (command == null)
      {
        return;
      }

      MenuItemCommand menuItemCommand = CreateMenuItem(command, commandTarget, parameter, gesture);

      if (menuItemCommand == null)
      {
        return;
      }

      MenuItemCommand existingCommand = commands.Find(menuItemInfo => menuItemInfo.Header == menuItemCommand.Header);

      if (existingCommand != null)
      {
        if (string.IsNullOrEmpty(existingCommand.InputGestureText))
        {
          existingCommand.InputGestureText = menuItemCommand.InputGestureText;
        }

        return;
      }

      commands.Add(menuItemCommand);
    }


    private MenuItemCommand CreateMenuItem(ICommand command, IInputElement commandTarget, object parameter,
                                        InputGesture gesture)
    {
      var itemCommand = new MenuItemCommand();

      itemCommand.Command = command;
      itemCommand.CommandTarget = commandTarget;
      itemCommand.CommandParameter = parameter;

      if (_extensions.ContainsKey(command))
      {
        IMenuManagerExtension extension = _extensions[command];
        itemCommand.Header = extension.GetCommandAlias(parameter);
      }
      else
      {
        GetItemText(itemCommand, parameter);
      }

      if (string.IsNullOrEmpty(itemCommand.Header))
      {
        return null;
      }

      if (gesture != null)
      {
        itemCommand.InputGestureText = GetInputGestureText(gesture);
      }

      if (string.IsNullOrEmpty(itemCommand.InputGestureText))
      {
        itemCommand.InputGestureText = GetInputGestureText(command);
      }

      return itemCommand;
    }


    private static string GetInputGestureText(ICommand command)
    {
      if (command is RoutedCommand)
      {
        var routedCommand = (RoutedCommand)command;

        return GetInputGestureText(routedCommand.InputGestures);
      }

      return null;
    }


    private static string GetInputGestureText(InputGestureCollection inputGestures)
    {
      for (int i = 0; i < inputGestures.Count; i++)
      {
        string gestureText = GetInputGestureText(inputGestures[i]);

        if (!string.IsNullOrEmpty(gestureText))
        {
          return gestureText;
        }
      }

      return null;
    }


    private static string GetInputGestureText(InputGesture gesture)
    {
      var keyGesture = gesture as KeyGesture;

      return keyGesture != null ? keyGesture.GetDisplayStringForCulture(CultureInfo.CurrentCulture) : null;
    }


    private void GetItemText(MenuItemCommand menuItemCommand, object parameter)
    {
      ICommand command = menuItemCommand.Command;

      menuItemCommand.Header = GetItemHeader(command, parameter);
    }


    private string GetItemHeader(ICommand command, object parameter)
    {
      if (command is RoutedUICommand)
      {
        return ((RoutedUICommand)command).Text;
      }

      if (command is RoutedCommand)
      {
        return ((RoutedCommand)command).Name;
      }

      return null;
    }


    private static void SettingsFileNamePropertyChanged(DependencyObject dependencyObject,
                                                        DependencyPropertyChangedEventArgs e)
    {
      MenuManager service = GetMenuManagerService(dependencyObject, e);

      if (service != null)
      {
        service.ProcessMenu((MenuBase)dependencyObject);
      }
    }


    private static void ProvideMenuItemsPropertyChanged(DependencyObject dependencyObject,
                                                        DependencyPropertyChangedEventArgs e)
    {
      MenuManager service = GetMenuManagerService(dependencyObject, e);

      if (service != null)
      {
        service.ProcessMenu((MenuBase)dependencyObject);
      }
    }


    private static MenuManager GetMenuManagerService(DependencyObject dependencyObject,
                                                     DependencyPropertyChangedEventArgs e)
    {
      if (e.NewValue.Equals(e.OldValue))
      {
        return null;
      }

      if (!(dependencyObject is MenuBase))
      {
        throw new InvalidOperationException(
          "ProvideMenuItems Property can only be set on classes derived from MenuBase.");
      }

      var service = (MenuManager)WtfFramework.Instance.Services[typeof(MenuManager)];

      if (service == null)
      {
        throw new InvalidOperationException("MenuItemProvider service not found.");
      }

      var menu = (MenuBase)dependencyObject;

      if (!_pendingMenus.Contains(menu))
      {
        _pendingMenus.Add(menu);
      }

      return service.State != FrameworkComponentState.Initialized ? null : service;
    }


    protected override void OnStateChnaged()
    {
      if (State == FrameworkComponentState.Initialized)
      {
        foreach (MenuBase menu in _pendingMenus)
        {
          ProcessMenu(menu);
        }
      }
      else
      {
        var menus = new List<MenuBase>();

        foreach (MenuState menuInfo in _menuTargets.Values)
        {
          menus.Add(menuInfo.Menu);
        }

        foreach (MenuBase menu in menus)
        {
          RemoveMenu(menu);
        }
      }
    }


    protected override void OnUnload()
    {
    }


    protected override FrameworkComponentState OnInitialize()
    {
      return FrameworkComponentState.Initialized;
    }


    protected override FrameworkComponentState OnLoad()
    {
      return FrameworkComponentState.Loaded;
    }


    protected override void OnExtensionRemoved(Service extension)
    {
      if (extension is IMenuManagerExtension)
      {
        var headerProvider = (IMenuManagerExtension)extension;

        if (!_extensions.ContainsKey(headerProvider.Command))
        {
          // TODO <RNovitsky, 28.05.2009>: Log this.

          return;
        }

        _extensions.Remove(headerProvider.Command);
      }
    }


    protected override void OnExtensionAdded(Service extension)
    {
      if (extension is IMenuManagerExtension)
      {
        var headerProvider = (IMenuManagerExtension)extension;
        _extensions.Add(headerProvider.Command, headerProvider);
      }
    }


    public override bool IsExtension(Service service)
    {
      if (service is IMenuManagerExtension)
      {
        var headerProvider = (IMenuManagerExtension)service;

        if (_extensions.ContainsKey(headerProvider.Command))
        {
          // TODO <RNovitsky, 28.05.2009>: Log this.

          return false;
        }

        return true;
      }

      return false;
    }


    public static void SetTargetCommandName(DependencyObject element, string value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(TargetCommandNameProperty, value);
    }


    public static string GetTargetCommandName(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (string)element.GetValue(TargetCommandNameProperty);
    }


    public static void SetIsDefault(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(IsDefaultProperty, value);
    }


    public static bool GetIsDefault(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(IsDefaultProperty);
    }


    public static void SetIsHidden(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(IsHiddenProperty, value);
    }


    public static bool GetIsHidden(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(IsHiddenProperty);
    }


    public static void SetIsPinned(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(IsPinnedProperty, value);
    }


    public static bool GetIsPinned(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(IsPinnedProperty);
    }


    public static void SetProvideMenuItems(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(ProvideMenuItemsProperty, value);
    }


    public static bool GetProvideMenuItems(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(ProvideMenuItemsProperty);
    }


    #endregion
  }
}